#!/usr/bin/ruby

# xfuzz - XSS Fuzzer
# Copyright (C) 2011 Ryan Dewhurst
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Contact: ryandewhurst at gmail


require 'rubygems'
require 'getoptlong'
require 'net/https'
require 'net/http'
require 'uri'
require 'cgi'
require 'digest/md5'

# Global Variables #

$version = 'BETA'
$release_date = '20/02/2011'
$verbose = false
target = ''
input_file = ''
$user_agent = "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9"
$results = Hash.new

def puts_banner()
  puts
  puts "_________________________________________________________________________"
  puts
  puts "                       o888o                                             "                             
  puts "         oooo   oooo o888oo oooo  oooo  ooooooooooo ooooooooooo          "
  puts "           888o888    888    888   888       8888        8888            "
  puts "           o88 88o    888    888   888    8888        8888               "    
  puts "         o88o   o88o o888o    888o88 8o o888ooooooo o888ooooooo          "
  puts "                                                     XSS Fuzzer          "
  puts "_________________________________________________________________________"
  puts
  puts " Version: " + $version + "                                             " 
  puts " Released: " + $release_date + "                                       "
  puts " Author: Ryan Dewhurst / www.ethicalhack3r.co.uk                       "
  puts " Project: http://code.google.com/p/xfuzz/                              "
  puts 
  puts " Usage: ./xfuzz.rb --target http://www.example.com/index.php?id=XSS  "
  puts "        ./xfuzz.rb --target http://www.example.com/index.php?id=XSS -v"
  puts "        ./xfuzz.rb --target https://www.example.com/index.php?id=XSS --file wordlists/html_tags.txt"
  puts
end

opts = GetoptLong.new(
  [ '--target', GetoptLong::OPTIONAL_ARGUMENT ],
  [ '--post', GetoptLong::OPTIONAL_ARGUMENT ],
  [ '--file', GetoptLong::OPTIONAL_ARGUMENT ],
  ['-v', GetoptLong::OPTIONAL_ARGUMENT]
) 

begin
  opts.each do |opt, arg|
    case opt
    when '--target'
      target = arg
    when '--post'
      method = arg
    when '--file'
      input_file = arg
    when '-v'
      $verbose = true
    end
  end
rescue
  puts_banner()
end

def check_file_exists( input_file )
  begin
    file = File.open(input_file, "r")
    file.close
  rescue
    puts "[ERROR] File not found."
    exit
  end
end

def check_target( target )
	if target !~ /^https?:\/\//i
		target = "http://" + target
	end
  if target !~ /XSS/
    puts "[ERROR] The target needs to include the keyword 'XSS' where the XSS vulnerability resides."
    exit
  end
	return target
end

def http_client( target )
  #todo: fix https, implement post
  
  url = URI.parse(URI.encode(target))
  http = Net::HTTP.new(url.host, url.port)
  
  if url.port == 443
  	http.use_ssl = true
  	http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end
  
  #puts http.use_ssl
	
#	if method == "post"
#	  request = Net::HTTP.post_form(url.path, {'q'=>'ruby'})
#	else
	  #request = Net::HTTP::Get.new(url.path + '?' + url.query)
	#end
	
	begin
    if url.query.nil?
      request = Net::HTTP::Get.new(url.path)
    else
      request = Net::HTTP::Get.new(url.path + '?' + url.query)
    end
  
    request.initialize_http_header({"User-Agent" => $user_agent})
  
		resp = Net::HTTP.start(url.host, url.port) {|http|
			  http.request(request)
		}
		
  	case resp
  		when Net::HTTPSuccess then resp
  		when Net::HTTPRedirection
        #http_client( resp['location'] ) # Un-comment if you want to follow 3xx redirects.
  	else
  		resp.error!
  	end
  rescue Timeout::Error
    resp = 'timeout'
  rescue => e
    puts e.inspect
  end
	
	puts '[+] Request: ' + target + ' [' + resp.code + ']' if $verbose
	
	#puts resp.inspect
	#puts resp.body
	return resp
end

def pre_fuzz_checks( target )
  
  return_string = false
  filter_input = false
  limit_size = 0
  randomstring = Digest::MD5.hexdigest(Time.now.to_s + (0..26).map{ ('a'..'z').to_a[rand(26)] }.join)

  puts "[STARTING] Pre-fuzz checks"
  puts
  
  puts '[?] Does the page return strings? Trying ' + randomstring.slice(0..6)
  
  if /#{randomstring.slice(0..6)}/ =~ http_client( target.gsub(/XSS/,randomstring.slice(0..6)) ).body
    puts '[A] Yes'
    return_string = true
  else
    puts '[A] No'
  end
  
  puts
  puts '[?] Does the page significantly limit the string size? '
  
  if /#{randomstring*10}/ =~ http_client( target.gsub(/XSS/,randomstring*10) ).body
    puts '[A] No. The page successfully returned all ' + (randomstring*10).length.to_s  + ' characters.'
  else
    randomstring = randomstring*10
    (0..320).each do |i|
      randomstring.chop!
      if /#{randomstring}/ =~ http_client( target.gsub(/XSS/,randomstring) ).body
        limit_size = randomstring.length
        puts '[A] Yes. We are limited to ' + limit_size.to_s + ' characters.'
        break
      end
      i-1
    end
  end
    
  puts
  puts '[?] Are our special characters relfected \'as-is\' within the page? Trying \'\';!--"<XSS>=&{()}'
  
  if /\'\'\;\!\-\-\"\<XSS\>\=\&\{\(\)\}/ =~ http_client( target.gsub(/XSS/,'\'\';!--"<XSS>=&{()}') ).body
    puts '[A] Yes'
  else
    puts '[A] No'
    filter_input = false
  end
  
  if return_string && filter_input == false
    puts
    puts '[+] Let\'s get fuzzing!'
    return true
  else
    puts
    puts '[+] You don\'t need me...?!'
    exit!
  end
  
end

def string_manipulation( fuzz_string )
  #TODO: Add more complex string manipulations
  manipulation = Array.new
  manipulation.push(fuzz_string.capitalize) # capitalize
  manipulation.push(fuzz_string.upcase) # upcase
  
  return manipulation
end

def in_response( char_payload, html_response_body )
	char_payload = Regexp.escape(char_payload)
	in_response = "No "
	if /#{char_payload}/ =~ html_response_body
		in_response = "Yes"
  end
    return in_response
end

def fuzz( target, char, fuzz_type, reference, fuzz_id )

	randomstring = Digest::MD5.hexdigest(Time.now.to_s + (0..26).map{ ('a'..'z').to_a[rand(26)] }.join).slice(0..6)
	
	char_payload = char + randomstring
  response = http_client( target.gsub(/XSS/, char_payload) )
    
  if response == 'timeout'
  	puts id.to_s + "         Timeout            No             " + fuzz_type + "        None             " + char
  else
  	if in_response( char_payload, response.body ) == "Yes"
  	  $results[char] = "allowed"
    else
      $results[char] = "denied"
    end
    puts " " + fuzz_id + "      " + response.code.to_s + "                " + in_response( char_payload, response.body ) + "           " + fuzz_type + "         " + reference + "             " + char
	end
	
end

def main_fuzzer( target, input_file )

  #todo: implement UTF8 with chr(Encoding::UTF_8)
  #todo: add further encoding methods
  
  seperator = "==============================================================================================="
  fuzz_id = "00000"
  
  puts
  puts seperator
  puts "  ID    Response-Code   Payload-in-Response       Type            Reference         Payload "
  puts seperator
  
  # Try all lines from user supplied input file
  
  if input_file != ''
    file = File.open(input_file, "r") 
      lines = file.readlines.collect{|line| line.chomp} # removes '\n' from each line
    file.close
  
    lines.each do |line| 
      fuzz( target, line, "File Input", "None", fuzz_id)
      fuzz_id.next
    end
  end
  
  # Go through the ASCII table symbols using chr()
  
  (0..255).each do |char|
  	encoded_string = char.chr.to_s
    fuzz( target, encoded_string, "ASCII-Symbols", "ASCII-"+char.to_s, fuzz_id)
    fuzz_id.next!
    char = char + 1
  end
  
  # Go through the ASCII table HTML encoded using CGI.escapeHTML()
  
  (0..255).each do |char|
    encoded_string = CGI.escapeHTML(char.chr.to_s)
    fuzz( target, encoded_string, "ASCII-HTML", "ASCII-"+char.to_s, fuzz_id)
    fuzz_id.next!
    char = char + 1
  end
  
  # Go through the UTF-8 table using chr(Encoding::UTF_8)
  
  #(0..255).each do |char|
  #  encoded_string = char.chr(Encoding::UTF_8).to_s
  #  fuzz( target, encoded_string, "UTF-8", "UTF8-"+char.to_s, fuzz_id)
  #  fuzz_id = fuzz_id + 1
  #  char = char + 1
  #end
  
  puts seperator
  
  finalise()
    
end

def finalise()
  
  allowed = Array.new
  denied = Array.new

  puts
  puts "Allowed:"
  puts
  
  $results.each do |payload,result|
     allowed.push(payload) if result == "allowed"
  end
  
  puts allowed.inspect
  
  puts
  puts "Denied:"
  puts
  
  $results.each do |payload,result|
   denied.push(payload) if result == "denied"   
  end
  
  puts denied.inspect
  
  puts
  puts "##############################################################################################################"
  puts "# Thanks to all BETA testers, the authors of Web Application Obfuscation and every one in infosec community. #"
  puts "#                                                                                                            #"
  puts "# References:                                                                                                #"
  puts "# http://ha.ckers.org/xss.html                                                                               #"
  puts "# Web Application Obfuscation - ISBN-13: 978-1597496049                                                      #"
  puts "# EnDe/EMiR - https://github.com/EnDe/EMiR                                                                   #"
  puts "#                                                                                                            #"
  puts "# Bugs or feature requests? http://code.google.com/p/xfuzz/                                                  #"
  puts "##############################################################################################################"
  puts
end

# Main #

begin
  puts_banner()
  if target != ''
    if input_file != ''
      check_file_exists( input_file )
    end
    target = check_target( target )
    pre_fuzz_checks( target )
    main_fuzzer( target, input_file )
  end
end